/*
 * Conditions Of Use 
 * 
 * This software was developed by employees of the National Institute of
 * Standards and Technology (NIST), an agency of the Federal Government.
 * Pursuant to title 15 Untied States Code Section 105, works of NIST
 * employees are not subject to copyright protection in the United States
 * and are considered to be in the public domain.  As a result, a formal
 * license is not needed to use the software.
 * 
 * This software is provided by NIST as a service and is expressly
 * provided "AS IS."  NIST MAKES NO WARRANTY OF ANY KIND, EXPRESS, IMPLIED
 * OR STATUTORY, INCLUDING, WITHOUT LIMITATION, THE IMPLIED WARRANTY OF
 * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE, NON-INFRINGEMENT
 * AND DATA ACCURACY.  NIST does not warrant or make any representations
 * regarding the use of the software or the results thereof, including but
 * not limited to the correctness, accuracy, reliability or usefulness of
 * the software.
 * 
 * Permission to use this software is contingent upon your acceptance
 * of the terms of this agreement
 *  
 * .
 * 
 */

/*******************************************************************************
 * Product of NIST/ITL Advanced Networking Technologies Division (ANTD)        *
 ******************************************************************************/

package gov.nist.javax.sip.parser;

import gov.nist.core.Host;
import gov.nist.core.HostNameParser;
import gov.nist.javax.sip.SIPConstants;
import gov.nist.javax.sip.address.AddressImpl;
import gov.nist.javax.sip.address.GenericURI;
import gov.nist.javax.sip.address.SipUri;
import gov.nist.javax.sip.address.TelephoneNumber;
import gov.nist.javax.sip.header.*;
import gov.nist.javax.sip.message.SIPMessage;
import gov.nist.javax.sip.message.SIPRequest;
import gov.nist.javax.sip.message.SIPResponse;

import java.io.UnsupportedEncodingException;
import java.text.ParseException;
/*
 * Acknowledgement: 1/12/2007: Yanick Belanger rewrote the parsing loops to make them
 * simpler and quicker.
 */

/**
 * Parse SIP message and parts of SIP messages such as URI's etc from memory and
 * return a structure. Intended use: UDP message processing. This class is used
 * when you have an entire SIP message or SIPHeader or SIP URL in memory and you
 * want to generate a parsed structure from it. For SIP messages, the payload
 * can be binary or String. If you have a binary payload, use
 * parseSIPMessage(byte[]) else use parseSIPMessage(String) The payload is
 * accessible from the parsed message using the getContent and getContentBytes
 * methods provided by the SIPMessage class. If SDP parsing is enabled using the
 * parseContent method, then the SDP body is also parsed and can be accessed
 * from the message using the getSDPAnnounce method. Currently only eager
 * parsing of the message is supported (i.e. the entire message is parsed in one
 * feld swoop).
 * 
 * 
 * @version 1.2 $Revision: 1.23 $ $Date: 2007/11/04 23:21:16 $
 * 
 * @author M. Ranganathan <br/>
 * 
  * 
 */
public class StringMsgParser {

	protected boolean readBody;
	private ParseExceptionListener parseExceptionListener;
	private String rawStringMessage;
	
	private static boolean computeContentLengthFromMessage = false;

	/**
	 * @since v0.9
	 */
	public StringMsgParser() {
		super();
		readBody = true;
	}

	/**
	 * Constructor (given a parse exception handler).
	 * 
	 * @since 1.0
	 * @param exhandler
	 *            is the parse exception listener for the message parser.
	 */
	public StringMsgParser(ParseExceptionListener exhandler) {
		this();
		parseExceptionListener = exhandler;
	}

	/**
	 * Add a handler for header parsing errors.
	 * 
	 * @param pexhandler
	 *            is a class that implements the ParseExceptionListener
	 *            interface.
	 */
	public void setParseExceptionListener(ParseExceptionListener pexhandler) {
		parseExceptionListener = pexhandler;
	}

	/**
	 * Parse a buffer containing a single SIP Message where the body is an array
	 * of un-interpreted bytes. This is intended for parsing the message from a
	 * memory buffer when the buffer. Incorporates a bug fix for a bug that was
	 * noted by Will Sullin of Callcast
	 * 
	 * @param msgBuffer
	 *            a byte buffer containing the messages to be parsed. This can
	 *            consist of multiple SIP Messages concatenated together.
	 * @return a SIPMessage[] structure (request or response) containing the
	 *         parsed SIP message.
	 * @exception ParseException
	 *                is thrown when an illegal message has been encountered
	 *                (and the rest of the buffer is discarded).
	 * @see ParseExceptionListener
	 */
	public SIPMessage parseSIPMessage(byte[] msgBuffer) throws ParseException {
		if (msgBuffer == null || msgBuffer.length == 0)
			return null;

		int i = 0;
		
		// Squeeze out any leading control character.
		try {
			while (msgBuffer[i] < 0x20)
				i++;
		}
		catch (ArrayIndexOutOfBoundsException e) {
			// Array contains only control char, return null.
			return null;
		}
		
		// Iterate thru the request/status line and headers.
		String currentLine = null;
		String currentHeader = null;
		boolean isFirstLine = true;
		SIPMessage message = null;
		do
		{
			int lineStart = i;
			
			// Find the length of the line.
			try {
				while (msgBuffer[i] != '\r' && msgBuffer[i] != '\n')
					i++;
			}
			catch (ArrayIndexOutOfBoundsException e) {
				// End of the message.
				break;
			}
			int lineLength = i - lineStart;

			// Make it a String.
			try {
				currentLine = new String(msgBuffer, lineStart, lineLength, "UTF-8");
			} catch (UnsupportedEncodingException e) {
				throw new ParseException("Bad message encoding!", 0);
			}

			currentLine = trimEndOfLine(currentLine);

			if (currentLine.length() == 0) {
				// Last header line, process the previous buffered header.
				if (currentHeader != null && message != null) {
 					processHeader(currentHeader, message);
 				}
				
			}
			else {
				if (isFirstLine) {
					message = processFirstLine(currentLine);
				} else {
					char firstChar = currentLine.charAt(0);
					if (firstChar == '\t' || firstChar == ' ') {
						if (currentHeader == null)
							throw new ParseException("Bad header continuation.", 0);
	
						// This is a continuation, append it to the previous line.
						currentHeader += currentLine.substring(1);
					}
					else {
						if (currentHeader != null && message != null) {
 							processHeader(currentHeader, message);
 						}
						currentHeader = currentLine;
					}
				}
			}

			if (msgBuffer[i] == '\r' && msgBuffer.length > i+1 && msgBuffer[i+1] == '\n')
				i++;

			i++;

			isFirstLine = false;
		} while (currentLine.length() > 0); // End do - while
		
		if (message == null) throw new ParseException("Bad message", 0);
		message.setSize(i);
		
		if (readBody && message.getContentLength() != null &&
				message.getContentLength().getContentLength() != 0) {

			int bodyLength = msgBuffer.length - i;
			
			byte[] body = new byte[bodyLength];
			System.arraycopy(msgBuffer, i, body, 0, bodyLength);
			message.setMessageContent(body,computeContentLengthFromMessage ,message.getContentLength().getContentLength() );
		}
		
		return message;
	}

	/**
	 * Parse a buffer containing one or more SIP Messages and return an array of
	 * SIPMessage parsed structures.
	 * 
	 * @param msgString
	 *            a String containing the messages to be parsed. This can
	 *            consist of multiple SIP Messages concatenated together.
	 * @return a SIPMessage structure (request or response) containing the
	 *         parsed SIP message.
	 * @exception ParseException
	 *                is thrown when an illegal message has been encountered
	 *                (and the rest of the buffer is discarded).
	 * @see ParseExceptionListener
	 */
	public SIPMessage parseSIPMessage(String msgString) throws ParseException {
		if (msgString == null || msgString.length() == 0)
			return null;

		rawStringMessage = msgString;

		int i = 0;

		// Squeeze out any leading control character.
		try {
			while (msgString.charAt(i) < 0x20)
				i++;
		}
		catch (ArrayIndexOutOfBoundsException e) {
			// Array contains only control char, return null.
			return null;
		} catch (StringIndexOutOfBoundsException ex) {
			return null;
		}

		// Iterate thru the request/status line and headers.
		String currentLine = null;
		String currentHeader = null;
		boolean isFirstLine = true;
		SIPMessage message = null;
		do
		{
			int lineStart = i;

			// Find the length of the line.
			try {
				char c = msgString.charAt(i);
				while (c != '\r' && c != '\n')
					c = msgString.charAt(++i);
			}
			catch (ArrayIndexOutOfBoundsException e) {
				// End of the message.
				break;
			} catch ( StringIndexOutOfBoundsException ex) {
				break;
			}

			// Make it a String.
			currentLine = msgString.substring(lineStart, i);
			currentLine = trimEndOfLine(currentLine);

			if (currentLine.length() == 0) {
				// Last header line, process the previous buffered header.
				if (currentHeader != null) {
					processHeader(currentHeader, message);
				}
			}
			else {
				if (isFirstLine) {
					message = processFirstLine(currentLine);
				} else {
					char firstChar = currentLine.charAt(0);
					if (firstChar == '\t' || firstChar == ' ') {
						if (currentHeader == null)
							throw new ParseException("Bad header continuation.", 0);

						// This is a continuation, append it to the previous line.
						currentHeader += currentLine.substring(1);
					}
					else {
						if (currentHeader != null) {
							processHeader(currentHeader, message);
						}
						currentHeader = currentLine;
					}
				}
			}

			if (msgString.charAt(i) == '\r' && msgString.length() > i+1 && msgString.charAt(i+1) == '\n')
				i++;

			i++;

			isFirstLine = false;
		}
		while (currentLine.length() > 0);

		message.setSize(i);

		// Check for content legth header
		if (readBody && message.getContentLength() != null &&
				message.getContentLength().getContentLength() != 0) {
			String body = msgString.substring(i);
			message.setMessageContent(body,computeContentLengthFromMessage,message.getContentLength().getContentLength());
			
		}

		return message;
	}

	private String trimEndOfLine(String line) {
		if (line == null)
			return line;
		
		int i = line.length() - 1;
		while (i >= 0 && line.charAt(i) <= 0x20)
			i--;
		
		if (i == line.length() - 1)
			return line;
		
		if (i == -1)
			return "";
		
		return line.substring(0, i+1);
	}

	private SIPMessage processFirstLine(String firstLine) throws ParseException {
		SIPMessage message;
		if (!firstLine.startsWith(SIPConstants.SIP_VERSION_STRING)) {
			message = new SIPRequest();
			try {
				RequestLine requestLine = new RequestLineParser(firstLine + "\n")
						.parse();
				((SIPRequest) message).setRequestLine(requestLine);
			} catch (ParseException ex) {
				if (this.parseExceptionListener != null)
					this.parseExceptionListener.handleException(ex, message,
							RequestLine.class, firstLine, rawStringMessage);
				else
					throw ex;

			}
		} else {
			message = new SIPResponse();
			try {
				StatusLine sl = new StatusLineParser(firstLine + "\n").parse();
				((SIPResponse) message).setStatusLine(sl);
			} catch (ParseException ex) {
				if (this.parseExceptionListener != null) {
					this.parseExceptionListener.handleException(ex, message,
							StatusLine.class, firstLine, rawStringMessage);
				} else
					throw ex;

			}
		}
		return message;
	}

	private void processHeader(String header, SIPMessage message) throws ParseException {
		if (header == null || header.length() == 0)
			return;
		
		HeaderParser headerParser = null;
		try {
			headerParser = ParserFactory.createParser(header + "\n");
		} catch (ParseException ex) {
			this.parseExceptionListener.handleException(ex, message, null,
					header, rawStringMessage);
			return;
		}
		
		try {
			SIPHeader sipHeader = headerParser.parse();
			message.attachHeader(sipHeader, false);
		} catch (ParseException ex) {
			if (this.parseExceptionListener != null) {
				String headerName = Lexer.getHeaderName(header);
				Class headerClass = NameMap.getClassFromName(headerName);
				if (headerClass == null) {
					headerClass = ExtensionHeaderImpl.class;

				}
				this.parseExceptionListener.handleException(ex, message,
						headerClass, header, rawStringMessage);

			}
		}
	}

	/**
	 * Parse an address (nameaddr or address spec) and return and address
	 * structure.
	 * 
	 * @param address
	 *            is a String containing the address to be parsed.
	 * @return a parsed address structure.
	 * @since v1.0
	 * @exception ParseException
	 *                when the address is badly formatted.
	 */
	public AddressImpl parseAddress(String address) throws ParseException {
		AddressParser addressParser = new AddressParser(address);
		return addressParser.address();
	}

	/**
	 * Parse a host:port and return a parsed structure.
	 * 
	 * @param hostport
	 *            is a String containing the host:port to be parsed
	 * @return a parsed address structure.
	 * @since v1.0
	 * @exception throws
	 *                a ParseException when the address is badly formatted.
	 *
	public HostPort parseHostPort(String hostport) throws ParseException {
		Lexer lexer = new Lexer("charLexer", hostport);
		return new HostNameParser(lexer).hostPort();

	}
	*/

	/**
	 * Parse a host name and return a parsed structure.
	 * 
	 * @param host
	 *            is a String containing the host name to be parsed
	 * @return a parsed address structure.
	 * @since v1.0
	 * @exception ParseException
	 *                a ParseException when the hostname is badly formatted.
	 */
	public Host parseHost(String host) throws ParseException {
		Lexer lexer = new Lexer("charLexer", host);
		return new HostNameParser(lexer).host();

	}

	/**
	 * Parse a telephone number return a parsed structure.
	 * 
	 * @param telephone_number
	 *            is a String containing the telephone # to be parsed
	 * @return a parsed address structure.
	 * @since v1.0
	 * @exception ParseException
	 *                a ParseException when the address is badly formatted.
	 */
	public TelephoneNumber parseTelephoneNumber(String telephone_number)
			throws ParseException {
		// Bug fix contributed by Will Scullin
		return new URLParser(telephone_number).parseTelephoneNumber();

	}

	/**
	 * Parse a SIP url from a string and return a URI structure for it.
	 * 
	 * @param url
	 *            a String containing the URI structure to be parsed.
	 * @return A parsed URI structure
	 * @exception ParseException
	 *                if there was an error parsing the message.
	 */

	public SipUri parseSIPUrl(String url) throws ParseException {
		try {
			return new URLParser(url).sipURL();
		} catch (ClassCastException ex) {
			throw new ParseException(url + " Not a SIP URL ", 0);
		}
	}

	/**
	 * Parse a uri from a string and return a URI structure for it.
	 * 
	 * @param url
	 *            a String containing the URI structure to be parsed.
	 * @return A parsed URI structure
	 * @exception ParseException
	 *                if there was an error parsing the message.
	 */

	public GenericURI parseUrl(String url) throws ParseException {
		return new URLParser(url).parse();
	}

	/**
	 * Parse an individual SIP message header from a string.
	 * 
	 * @param header
	 *            String containing the SIP header.
	 * @return a SIPHeader structure.
	 * @exception ParseException
	 *                if there was an error parsing the message.
	 */
	public SIPHeader parseSIPHeader(String header) throws ParseException {
		int start = 0;
		int end = header.length() - 1;
		try {
			// Squeeze out any leading control character.
			while (header.charAt(start) <= 0x20)
				start++;
			
			// Squeeze out any trailing control character.
			while (header.charAt(end) <= 0x20)
				end--;
		}
		catch (ArrayIndexOutOfBoundsException e) {
			// Array contains only control char.
			throw new ParseException("Empty header.", 0);
		}
		
		StringBuffer buffer = new StringBuffer(end + 1);
		int i = start;
		int lineStart = start;
		boolean endOfLine = false;
		while (i <= end) {
			char c = header.charAt(i);
			if (c == '\r' || c == '\n') {
				if (!endOfLine) {
					buffer.append(header.substring(lineStart, i));
					endOfLine = true;
				}
			}
			else {
				if (endOfLine) {
					endOfLine = false;
					if (c == ' ' || c == '\t') {
						buffer.append(' ');
						lineStart = i + 1;
					}
					else {
						lineStart = i;
					}
				}
			}
			
			i++;
		}
		buffer.append(header.substring(lineStart, i));
		buffer.append('\n');
		
		HeaderParser hp = ParserFactory.createParser(buffer.toString());
		if (hp == null)
			throw new ParseException("could not create parser", 0);
		return hp.parse();
	}

	/**
	 * Parse the SIP Request Line
	 * 
	 * @param requestLine
	 *            a String containing the request line to be parsed.
	 * @return a RequestLine structure that has the parsed RequestLine
	 * @exception ParseException
	 *                if there was an error parsing the requestLine.
	 */

	public RequestLine parseSIPRequestLine(String requestLine)
			throws ParseException {
		requestLine += "\n";
		return new RequestLineParser(requestLine).parse();
	}

	/**
	 * Parse the SIP Response message status line
	 * 
	 * @param statusLine
	 *            a String containing the Status line to be parsed.
	 * @return StatusLine class corresponding to message
	 * @exception ParseException
	 *                if there was an error parsing
	 * @see StatusLine
	 */

	public StatusLine parseSIPStatusLine(String statusLine)
			throws ParseException {
		statusLine += "\n";
		return new StatusLineParser(statusLine).parse();
	}

	public static void setComputeContentLengthFromMessage(
			boolean computeContentLengthFromMessage) {
		StringMsgParser.computeContentLengthFromMessage = computeContentLengthFromMessage;
	}

	

	/**
	 * Test code.
	 */
	public static void main(String[] args) throws ParseException {
		String messages[] = {
				"SIP/2.0 200 OK\r\n"
						+ "To: \"The Little Blister\" <sip:LittleGuy@there.com>;tag=469bc066\r\n"
						+ "From: \"The Master Blaster\" <sip:BigGuy@here.com>;tag=11\r\n"
						+ "Via: SIP/2.0/UDP 139.10.134.246:5060;branch=z9hG4bK8b0a86f6_1030c7d18e0_17;received=139.10.134.246\r\n"
						+ "Call-ID: 1030c7d18ae_a97b0b_b@8b0a86f6\r\n"
						+ "CSeq: 1 SUBSCRIBE\r\n"
						+ "Contact: <sip:172.16.11.162:5070>\r\n"
						+ "Content-Length: 0\r\n\r\n",

				"SIP/2.0 180 Ringing\r\n"
						+ "Via: SIP/2.0/UDP 172.18.1.29:5060;branch=z9hG4bK43fc10fb4446d55fc5c8f969607991f4\r\n"
						+ "To: \"0440\" <sip:0440@212.209.220.131>;tag=2600\r\n"
						+ "From: \"Andreas\" <sip:andreas@e-horizon.se>;tag=8524\r\n"
						+ "Call-ID: f51a1851c5f570606140f14c8eb64fd3@172.18.1.29\r\n"
						+ "CSeq: 1 INVITE\r\n" + "Max-Forwards: 70\r\n"
						+ "Record-Route: <sip:212.209.220.131:5060>\r\n"
						+ "Content-Length: 0\r\n\r\n",
				"REGISTER sip:nist.gov SIP/2.0\r\n"
						+ "Via: SIP/2.0/UDP 129.6.55.182:14826\r\n"
						+ "Max-Forwards: 70\r\n"
						+ "From: <sip:mranga@nist.gov>;tag=6fcd5c7ace8b4a45acf0f0cd539b168b;epid=0d4c418ddf\r\n"
						+ "To: <sip:mranga@nist.gov>\r\n"
						+ "Call-ID: c5679907eb954a8da9f9dceb282d7230@129.6.55.182\r\n"
						+ "CSeq: 1 REGISTER\r\n"
						+ "Contact: <sip:129.6.55.182:14826>;methods=\"INVITE, MESSAGE, INFO, SUBSCRIBE, OPTIONS, BYE, CANCEL, NOTIFY, ACK, REFER\"\r\n"
						+ "User-Agent: RTC/(Microsoft RTC)\r\n"
						+ "Event:  registration\r\n"
						+ "Allow-Events: presence\r\n"
						+ "Content-Length: 0\r\n\r\n"
						+ "INVITE sip:littleguy@there.com:5060 SIP/2.0\r\n"
						+ "Via: SIP/2.0/UDP 65.243.118.100:5050\r\n"
						+ "From: M. Ranganathan  <sip:M.Ranganathan@sipbakeoff.com>;tag=1234\r\n"
						+ "To: \"littleguy@there.com\" <sip:littleguy@there.com:5060> \r\n"
						+ "Call-ID: Q2AboBsaGn9!?x6@sipbakeoff.com \r\n"
						+ "CSeq: 1 INVITE \r\n"
						+ "Content-Length: 247\r\n\r\n"
						+ "v=0\r\n"
						+ "o=4855 13760799956958020 13760799956958020 IN IP4  129.6.55.78\r\n"
						+ "s=mysession session\r\n" + "p=+46 8 52018010\r\n"
						+ "c=IN IP4  129.6.55.78\r\n" + "t=0 0\r\n"
						+ "m=audio 6022 RTP/AVP 0 4 18\r\n"
						+ "a=rtpmap:0 PCMU/8000\r\n"
						+ "a=rtpmap:4 G723/8000\r\n"
						+ "a=rtpmap:18 G729A/8000\r\n" + "a=ptime:20\r\n" };

		class ParserThread implements Runnable {
			String[] messages;

			public ParserThread(String[] messagesToParse) {
				this.messages = messagesToParse;
			}

			public void run() {
				for (int i = 0; i < messages.length; i++) {
					StringMsgParser smp = new StringMsgParser();
					try {
						SIPMessage sipMessage = smp
								.parseSIPMessage(messages[i]);
						System.out.println(" i = " + i + " branchId = "
								+ sipMessage.getTopmostVia().getBranch());
						// System.out.println("encoded " +
						// sipMessage.toString());
					} catch (ParseException ex) {

					}

					// System.out.println("dialog id = " +
					// sipMessage.getDialogId(false));
				}
			}
		}

		for (int i = 0; i < 20; i++) {
			new Thread(new ParserThread(messages)).start();
		}

	}

}